package rasterization;

import java.util.List;

import raytracing.Camera;
import scene.Scene;
import scene.accel.bsp.BSPTree;
import scene.shape.BoundingBox;
import scene.shape.Shape;
import scene.shape.geometry.Triangle;
import scene.shape.materials.Material;
import util.CgPanel;
import util.Color3f;
import util.Line2D;
import util.Matrix4f;
import util.Vector3f;
import util.Vector4f;

public class Rasterizer {
	
	private CgPanel panel;
	private float[][] zbuffer;
	private BSPTree bsp;

	public Rasterizer(CgPanel panel, boolean useBSPTree){
		this.panel = panel;
		if(useBSPTree){
			this.bsp = new BSPTree(Scene.getInstance().getTriangles());
		}else{
			zbuffer = new float[panel.getWidth()][panel.getHeight()];
			for(int i = 0; i < panel.getWidth(); i++){
				for(int j = 0; j < panel.getHeight(); j++){
					zbuffer[i][j] = Float.POSITIVE_INFINITY;
				}
			}
		}
		
	}
	
	public void drawWorldSpaceLine(Vector3f a, Vector3f b){
		if(a == null || b == null)
			return;
		Matrix4f m = Scene.getInstance().getCurrentCamera().getFullMatrix(-1f, -100f);
		Vector4f v1 = m.multiply(new Vector4f(a));
		Vector4f v2 = m.multiply(new Vector4f(b));
		drawLine(new Line2D(v1.x / v1.w, v1.y / v1.w, v2.x / v2.w, v2.y / v2.w));
	}
	
	public void drawLine(Line2D l){
		float m = l.getSlope();
		if(m >= 0){
			if(m <= 1)
				drawLineSlopeZeroToOne(l);
			else
				drawLineSlopeOneToInfinity(l);
		}
		else{
			if(m >= -1)
				drawLineSlopeZeroToMinusOne(l);
			else
				drawLineSlopeMinusOneToMinusInfinity(l);
		}
			
	}
	
	/**
	 * Draw triangle, using Z-buffering
	 * @param t
	 * @param worldSpaceTriangle
	 * @param m
	 */
	public void drawTriangle(Triangle t, Triangle worldSpaceTriangle, Material m){
		int ymin = (int) Math.min(t.v1.y, Math.min(t.v2.y, t.v3.y)),
			ymax = (int) Math.max(t.v1.y, Math.max(t.v2.y, t.v3.y)),
			xmin = (int) Math.min(t.v1.x, Math.min(t.v2.x, t.v3.x)),
			xmax = (int) Math.max(t.v1.x, Math.max(t.v2.x, t.v3.x));
		float x0 = t.v1.x, y0 = t.v1.y,
			  x1 = t.v2.x, y1 = t.v2.y,
			  x2 = t.v3.x, y2 = t.v3.y,
			  falpha = f12(x0,y0,t),
			  fbeta = f20(x1,y1,t),
			  fgamma = f01(x2,y2,t);
		
		for(int y = ymin; y <= ymax; y++){
			for(int x = xmin; x <= xmax; x++){
				float alpha = f12(x,y,t) / falpha,
					  beta = f20(x,y,t) / fbeta,
					  gamma = f01(x,y,t) / fgamma;
				if(alpha >= 0 && beta >= 0 && gamma >= 0){
					if((alpha > 0 || falpha * f12(-1,-1,t) > 0) &&
							(beta > 0 || fbeta * f20(-1,-1,t) > 0 ) &&
							(gamma > 0 || fgamma * f01(-1,-1,t) > 0)){
						//Color3f c = t.c1.scale(alpha).add(t.c2.scale(beta)).add(t.c3.scale(gamma));
						Color3f c = m.shade(worldSpaceTriangle, alpha, beta, gamma);
						float z = t.getZ(alpha,beta,gamma);
						if( z > zbuffer[x-1][y-1]){
							zbuffer[x-1][y-1] = z;
							panel.drawPixel(x , panel.getHeight() - y, c);
						}
					}
				}
			}
		}
	}
	
	/**
	 * Rasterize a triangle, without Z-buffering, for use with BSP-tree
	 * @param t
	 * @param worldSpaceTriangle
	 * @param m
	 */
	public void rasterizeTriangle(Triangle t, Triangle worldSpaceTriangle, Material m){
		int ymin = (int) Math.min(t.v1.y, Math.min(t.v2.y, t.v3.y)),
			ymax = (int) Math.max(t.v1.y, Math.max(t.v2.y, t.v3.y)),
			xmin = (int) Math.min(t.v1.x, Math.min(t.v2.x, t.v3.x)),
			xmax = (int) Math.max(t.v1.x, Math.max(t.v2.x, t.v3.x));
		float x0 = t.v1.x, y0 = t.v1.y,
			  x1 = t.v2.x, y1 = t.v2.y,
			  x2 = t.v3.x, y2 = t.v3.y,
			  falpha = f12(x0,y0,t),
			  fbeta = f20(x1,y1,t),
			  fgamma = f01(x2,y2,t);
		for(int y = ymin; y <= ymax; y++){
			for(int x = xmin; x <= xmax; x++){
				float alpha = f12(x,y,t) / falpha,
					  beta = f20(x,y,t) / fbeta,
					  gamma = f01(x,y,t) / fgamma;
				if(alpha >= 0 && beta >= 0 && gamma >= 0){
					if((alpha > 0 || falpha * f12(-1,-1,t) > 0) &&
							(beta > 0 || fbeta * f20(-1,-1,t) > 0 ) &&
							(gamma > 0 || fgamma * f01(-1,-1,t) > 0)){
						//Color3f c = t.c1.scale(alpha).add(t.c2.scale(beta)).add(t.c3.scale(gamma));
						Color3f c = m.shade(worldSpaceTriangle, alpha, beta, gamma);
						panel.drawPixel(x , panel.getHeight() - y, c);
					}
				}
			}
		}
	}
	
	private float f12(float x, float y, Triangle t){
		float 	  x1 = t.v2.x, y1 = t.v2.y,
				  x2 = t.v3.x, y2 = t.v3.y;
		return (y1 - y2) * x + (x2 - x1) * y + x1*y2 - x2*y1;
	}
	private float f20(float x, float y, Triangle t){
		float 	  x0 = t.v1.x, y0 = t.v1.y,
				  x2 = t.v3.x, y2 = t.v3.y;
		return (y2 - y0) * x + (x0 - x2) * y + x2*y0 - x0*y2;
	}
	
	private float f01(float x, float y, Triangle t){
		float 	  x0 = t.v1.x, y0 = t.v1.y,
				  x1 = t.v2.x, y1 = t.v2.y;
		return (y0 - y1) * x + (x1 - x0) * y + x0*y1 - x1*y0;
	}
	
	private void drawLineSlopeZeroToOne(Line2D l){
		if(l.a.x > l.b.x)
			l.swapAandB(); 
		int y = (int)l.a.y;
		float d = l.implicit(l.a.x+1, l.a.y+0.5f);
		for(int x = (int)(l.a.x); x < (int)l.b.x; x++ ){
			panel.drawPixel(x, panel.getHeight() - y, l.getColor());
			if(d < 0){
				y++;
				d = d + (l.b.x - l.a.x) + (l.a.y - l.b.y);
			}else{
				d = d + (l.a.y - l.b.y); 
			}
		}
	}
	
	private void drawLineSlopeOneToInfinity(Line2D l){
		if(l.a.y > l.b.y)
			l.swapAandB();
		int x = (int)l.a.x;
		float d = l.implicit(l.a.x+1, l.a.y+0.5f);
		for(int y = (int)(l.a.y); y < (int)l.b.y; y++ ){
			panel.drawPixel(x, panel.getHeight() - y, l.getColor());
			if(d > 0){
				x++;
				d += (l.b.x - l.a.x) + (l.a.y - l.b.y);
			}else{
				d += (l.b.x - l.a.x);//d=d+(x1-x0)
			}
		}
	}
	
	private void drawLineSlopeMinusOneToMinusInfinity(Line2D l){
		if(l.a.y > l.b.y)
			l.swapAandB();
		int x = (int)l.a.x;
		float d = l.implicit(l.a.x+1, l.a.y+0.5f);
		for(int y = (int)(l.a.y); y < (int)l.b.y; y++ ){
			panel.drawPixel(x, panel.getHeight() - y, l.getColor());
			if(d < 0){
				x--;
				d += (l.b.x - l.a.x) - (l.a.y - l.b.y);
			}else{
				d += (l.b.x - l.a.x); 
			}
		}
	}
	
	private void drawLineSlopeZeroToMinusOne(Line2D l){
		if(l.a.x > l.b.x)
			l.swapAandB();
		int y = (int)l.a.y;
		float d = l.implicit(l.a.x+1, l.a.y+0.5f);
		for(int x = (int)(l.a.x); x < (int)l.b.x; x++ ){
			panel.drawPixel(x, panel.getHeight() - y, l.getColor());
			if(d > 0){
				y--;
				d += -(l.b.x - l.a.x) + (l.a.y - l.b.y);
			}else{
				d += l.a.y - l.b.y; 
			}
		}
	}
	
	public void resetZBuffer(){
		//zbuffer = new float[panel.getWidth()][panel.getHeight()];
		zbuffer = new float[1500][1500];
		for(int i = 0; i < panel.getWidth(); i++){
			for(int j = 0; j < panel.getHeight(); j++){
				zbuffer[i][j] = Float.NEGATIVE_INFINITY;
			}
		}
	}

	public void renderScene() {
		panel.clear();
		Scene.getInstance().getCurrentCamera().setNx(panel.getWidth());
		Scene.getInstance().getCurrentCamera().setNy(panel.getHeight());
		for(int x = 0; x < panel.getWidth(); x++){
			for(int y = 0; y < panel.getHeight(); y++){
				if(Scene.getInstance().getCubeMap() != null){
				Camera cam = Scene.getInstance().getCurrentCamera();
				Vector3f d = cam.getW().add(cam.getV().scale(1f-2f*(float)y/(float)panel.getHeight())).add(cam.getU().scale(1f-2f*(float)x/(float)panel.getWidth()));
				Color3f c = Scene.getInstance().getCubeMap().getColor(d);
				panel.drawPixel(x, panel.getHeight() - y, c);
			}
			else
				panel.drawPixel(x, panel.getHeight() - y, Scene.getInstance().getBackgroundColor());
			}	
		}
		if(bsp != null){
			bsp.draw(Scene.getInstance().getCurrentCamera().getEye(), this);
		}
		else{
			resetZBuffer();
			System.out.println("Started drawing");
			for(Triangle t : Scene.getInstance().getTriangles()){
				renderTriangle(t);
			}	
		}
		
	}

	public void renderTriangle(Triangle t) {
		Matrix4f m = Scene.getInstance().getCurrentCamera().getFullMatrix(-1f, -100f);
		Vector4f a = m.multiply(t.getShape().getTransform().multiply(new Vector4f(t.v1))),
				 b = m.multiply(t.getShape().getTransform().multiply(new Vector4f(t.v2))),
				 c = m.multiply(t.getShape().getTransform().multiply(new Vector4f(t.v3)));
		Vector3f v1 = new Vector3f(a.x / a.w, a.y / a.w, a.z / a.w),
				 v2 = new Vector3f(b.x / b.w, b.y / b.w, b.z / b.w),
				 v3 = new Vector3f(c.x / c.w, c.y / c.w, c.z / c.w);
		drawTriangle(new Triangle(v1,v2,v3), t, t.getShape().getMaterial());
	}
	
	/**
	 * Rasterize triangle, without z-buffering (for use with BSP-tree)
	 * @param t
	 */
	public void rasterizeTriangle(Triangle t) {
		Matrix4f m = Scene.getInstance().getCurrentCamera().getFullMatrix(-1f, -100f);
		Vector4f a = m.multiply(t.getShape().getTransform().multiply(new Vector4f(t.v1))),
				 b = m.multiply(t.getShape().getTransform().multiply(new Vector4f(t.v2))),
				 c = m.multiply(t.getShape().getTransform().multiply(new Vector4f(t.v3)));
		Vector3f v1 = new Vector3f(a.x / a.w, a.y / a.w, a.z / a.w),
				 v2 = new Vector3f(b.x / b.w, b.y / b.w, b.z / b.w),
				 v3 = new Vector3f(c.x / c.w, c.y / c.w, c.z / c.w);
		rasterizeTriangle(new Triangle(v1,v2,v3), t, t.getShape().getMaterial());
	}
	
	public void renderWireFrame(List<Triangle> triangles, Color3f color){
		Scene.getInstance().getCurrentCamera().setNx(panel.getWidth());
		Scene.getInstance().getCurrentCamera().setNy(panel.getHeight());
		Matrix4f m = Scene.getInstance().getCurrentCamera().getFullMatrix(-1f, -100f);
		resetZBuffer();
		System.out.println("Started drawing");
			for(Triangle t : triangles){
				Vector4f a = m.multiply(new Vector4f(t.v1)),
						 b = m.multiply(new Vector4f(t.v2)),
						 c = m.multiply(new Vector4f(t.v3));
				Vector3f v1 = new Vector3f(a.x / a.w, a.y / a.w, a.z / a.w),
						 v2 = new Vector3f(b.x / b.w, b.y / b.w, b.z / b.w),
						 v3 = new Vector3f(c.x / c.w, c.y / c.w, c.z / c.w);
				drawLine(new Line2D(v1.x, v1.y, v2.x, v2.y));
				drawLine(new Line2D(v1.x, v1.y, v3.x, v3.y));
				drawLine(new Line2D(v2.x, v2.y, v3.x, v3.y));
			}
	}
	
	public void renderWireFrame(BoundingBox b){
		Vector3f minminmin = new Vector3f(b.lower.x, b.lower.y, b.lower.z),
		 		 minminmax = new Vector3f(b.lower.x, b.lower.y, b.upper.z),
				 minmaxmin = new Vector3f(b.lower.x, b.upper.y, b.lower.z),
				 maxminmin = new Vector3f(b.upper.x, b.lower.y, b.lower.z),
				 minmaxmax = new Vector3f(b.lower.x, b.upper.y, b.upper.z),
				 maxmaxmin = new Vector3f(b.upper.x, b.upper.y, b.lower.z),
				 maxminmax = new Vector3f(b.upper.x, b.lower.y, b.upper.z),
				 maxmaxmax = new Vector3f(b.upper.x, b.upper.y, b.upper.z);
		//left plane
		drawWorldSpaceLine(minminmin, minminmax);
		drawWorldSpaceLine(minminmax, minmaxmax);
		drawWorldSpaceLine(minmaxmax, minmaxmin);
		drawWorldSpaceLine(minmaxmin, minminmin);
		//baWorldSpaceck pl
		drawWorldSpaceLine(minmaxmin, maxmaxmin);
		drawWorldSpaceLine(maxmaxmin, maxminmin);
		drawWorldSpaceLine(maxminmin, minminmin);
		//riWorldSpaceght p
		drawWorldSpaceLine(maxmaxmin, maxmaxmax);
		drawWorldSpaceLine(maxmaxmax, maxminmax);
		drawWorldSpaceLine(maxminmax, maxminmin);
		//frWorldSpaceont p
		drawWorldSpaceLine(minmaxmax, maxmaxmax);
		drawWorldSpaceLine(minminmax, maxminmax);
	}

}
